﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MugenMvvmToolkit.Interfaces;
using MugenMvvmToolkit.Utils;

namespace MugenMvvmToolkit.Infrastructure
{
    /// <summary>
    ///     Represents the base entity state provider that uses the property to save state.
    /// </summary>
    public class EntityStateProvider : IEntityStateProvider
    {
        #region Nested type: EditObjectSavedState

        private class SavedState
        {
            #region Constructors

            /// <summary>
            ///     Initializes a new instance of the <see cref="SavedState" /> class.
            /// </summary>
            public SavedState(PropertyInfo propertyInfo, object value)
            {
                PropertyInfo = propertyInfo;
                Value = value;
            }

            #endregion

            #region Properties

            public readonly PropertyInfo PropertyInfo;

            public readonly object Value;

            #endregion
        }

        #endregion

        #region Fields

        private static readonly Dictionary<Type, IList<PropertyInfo>> TypesToProperties =
            new Dictionary<Type, IList<PropertyInfo>>();

        private readonly Dictionary<object, Dictionary<string, SavedState>> _savedStates;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityStateProvider"/> class.
        /// </summary>
        public EntityStateProvider(IEqualityComparer<object> equalityComparer = null)
        {
            if (equalityComparer == null)
                equalityComparer = EqualityComparer<object>.Default;
            _savedStates = new Dictionary<object, Dictionary<string, SavedState>>(equalityComparer);
        }

        #endregion

        #region Properties

        /// <summary>
        ///     Gets value that indicates that provider should throws an exception if entity not found in provider.
        /// </summary>
        protected virtual bool ThrowExceptionIfEntityNotFound
        {
            get { return true; }
        }

        #endregion

        #region Implementation of IEntityStateProvider

        /// <summary>
        ///     Gets a value indicating whether the provider supports changed detection.
        /// </summary>
        public virtual bool IsSupportChangeDetection
        {
            get
            {
                return true;
            }
        }

        /// <summary>
        ///     Saves a state of entity.
        /// </summary>
        /// <param name="entity">The specified entity.</param>
        /// <returns>An instance of object.</returns>
        public object Save(object entity)
        {
            Should.NotBeNull(entity, "entity");
            using (DeadLockDetector.Lock(_savedStates))
            {
                var savedStates = new Dictionary<string, SavedState>();
                IList<PropertyInfo> propertyInfos = GetPropertiesInternal(entity);
                foreach (PropertyInfo propertyInfo in propertyInfos)
                {
                    object oldState = propertyInfo.GetValue(entity, null);
                    savedStates.Add(propertyInfo.Name, new SavedState(propertyInfo, oldState));
                }
                _savedStates[entity] = savedStates;
                return entity;
            }
        }

        /// <summary>
        ///     Applies a state of entity.
        /// </summary>
        /// <param name="entity">The specified entity.</param>
        /// <returns>An instance of object.</returns>
        public object Apply(object entity)
        {
            Should.NotBeNull(entity, "entity");
            return entity;
        }

        /// <summary>
        ///     Cancels a state of entity.
        /// </summary>
        /// <param name="entity">The specified entity.</param>
        /// <returns>An instance of object.</returns>
        public object Cancel(object entity)
        {
            Should.NotBeNull(entity, "entity");
            using (DeadLockDetector.Lock(_savedStates))
            {
                Dictionary<string, SavedState> savedStates;
                if (!_savedStates.TryGetValue(entity, out savedStates))
                {
                    if (ThrowExceptionIfEntityNotFound)
                        Thrower.CancelChangesObjectNotFound();
                    return entity;
                }
                foreach (SavedState savedState in savedStates.Values)
                {
                    savedState.PropertyInfo.SetValue(entity, savedState.Value, null);
                }
                _savedStates.Remove(entity);
                return entity;
            }
        }

        /// <summary>
        ///     Gets a value indicating whether the entity has changes.
        /// </summary>
        public virtual bool HasChanges(object entity, string propertyName)
        {
            Should.NotBeNull(entity, "entity");
            Should.NotBeNull(propertyName, "propertyName");
            Dictionary<string, SavedState> savedStates;
            using (DeadLockDetector.Lock(_savedStates))
            {
                if (!_savedStates.TryGetValue(entity, out savedStates))
                    return false;
            }
            SavedState savedState;
            if (!savedStates.TryGetValue(propertyName, out savedState))
                return false;
            return !Equals(savedState.PropertyInfo.GetValue(entity, null), savedState.Value);
        }

        /// <summary>
        ///     Gets a value indicating whether the entity has changes.
        /// </summary>
        public virtual bool HasChanges(object entity)
        {
            Should.NotBeNull(entity, "entity");
            Dictionary<string, SavedState> savedStates;
            using (DeadLockDetector.Lock(_savedStates))
            {
                if (!_savedStates.TryGetValue(entity, out savedStates))
                    return false;
            }
            foreach (var savedState in savedStates.Values)
            {
                if (!Equals(savedState.PropertyInfo.GetValue(entity, null), savedState.Value))
                    return true;
            }
            return false;
        }

        #endregion

        #region Methods

        /// <summary>
        ///     Gets properties for save state.
        /// </summary>
        /// <param name="entity">The specified entity.</param>
        /// <returns>
        ///     A series of instances of <see cref="PropertyInfo" />.
        /// </returns>
        private IList<PropertyInfo> GetPropertiesInternal(object entity)
        {
            using (DeadLockDetector.Lock(TypesToProperties))
            {
                Type type = entity.GetType();
                IList<PropertyInfo> list;
                if (!TypesToProperties.TryGetValue(type, out list))
                {
                    bool shouldCache;
                    list = GetProperties(entity, out shouldCache);
                    if (shouldCache)
                        TypesToProperties[type] = list;
                }
                return list;
            }
        }

        /// <summary>
        ///     Gets properties for save state.
        /// </summary>
        /// <param name="entity">The specified entity.</param>
        /// <param name="shouldCache"></param>
        /// <returns>
        ///     A series of instances of <see cref="PropertyInfo" />.
        /// </returns>
        protected virtual IList<PropertyInfo> GetProperties(object entity, out bool shouldCache)
        {
            Type type = entity.GetType();
            shouldCache = true;
            return type.GetProperties()
                       .Where(info => info.CanRead && info.CanWrite)
                       .ToArray();
        }

        #endregion
    }
}